/*
 * Copyright 2012 Sergey Netesanyi.
 * 
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.google.gwt.user.rebind.rpc;

import com.google.gwt.core.client.GWT;
import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.GeneratorContextExt;
import com.google.gwt.core.ext.PropertyOracle;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JPackage;
import com.google.gwt.core.ext.typeinfo.JType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.dev.generator.NameFactory;
import com.google.gwt.dev.util.log.speedtracer.CompilerEventType;
import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger;
import com.google.gwt.dev.util.log.speedtracer.SpeedTracerLogger.Event;
import com.google.gwt.user.client.rpc.SerializationException;
import com.google.gwt.user.client.rpc.SerializationStreamReader;
import com.google.gwt.user.client.rpc.SerializationStreamWriter;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;
import com.versatile.gwt.serialization.client.ClientSerializable;
import com.versatile.gwt.serialization.client.ISerializer;
import com.versatile.gwt.serialization.client.PrimitivesWhiteList;
import com.versatile.gwt.serialization.client.stream.StringSerializationStreamReader;
import com.versatile.gwt.serialization.client.stream.StringSerializationStreamWriter;

import java.io.PrintWriter;
import java.util.Date;

/**
 * Creates a client-side implementation of the string serializer
 * {@link ISerializer} interface
 * as well as the necessary type and field serializers.
 *
 * @author Sergey Netesanyi
 * @version 1.0
 */
public class ClientSerializerCreator extends Generator {

    private static final String SUFFIX = "_Impl";

    protected JClassType serviceIntf;

    /**
     * Creates the client-side proxy class.
     *
     * @param logger
     *         Logger instance.
     * @param context
     *         Generator context.
     * @return Class Name.
     * @throws com.google.gwt.core.ext.UnableToCompleteException
     *         In case of error.
     */
    public String create(TreeLogger logger, GeneratorContextExt context)
            throws UnableToCompleteException {
        TypeOracle typeOracle = context.getTypeOracle();
        serviceIntf = typeOracle.findType(ISerializer.class.getCanonicalName());

        SourceWriter srcWriter = getSourceWriter(logger, context, serviceIntf);
        if (srcWriter == null) {
            return getProxyQualifiedName();
        }

        final PropertyOracle propertyOracle = context.getPropertyOracle();

        // Load the blacklist/whitelist
        BlackWhiteList blackWhiteList = getFilterList();
        blackWhiteList.build(logger, context);
        ClientSerializerTypeFilter serializerFilter = getTypeFilter(blackWhiteList);

        // Determine the set of serializable types
        SerializableTypeOracleBuilder typesSentFromBrowserBuilder =
                new SerializableTypeOracleBuilder(logger, propertyOracle, context);
        typesSentFromBrowserBuilder.setTypeFilter(serializerFilter);

        addRoots(logger, typeOracle, typesSentFromBrowserBuilder, blackWhiteList);

        SerializableTypeOracle serializableTypeOracle;
        serializableTypeOracle = typesSentFromBrowserBuilder.build(logger);

        generateTypeHandlers(logger, context, serializableTypeOracle);

        generateFields(srcWriter);
        // generate methods
        generateSerializeMethod(srcWriter);
        generateDeserializeMethod(srcWriter);
        generateIsSerializableMethod(srcWriter);

        srcWriter.commit(logger);

        return getProxyQualifiedName();
    }

    protected BlackWhiteList getFilterList() {
        return new BlackWhiteList();
    }

    protected ClientSerializerTypeFilter getTypeFilter(BlackWhiteList blackWhiteList)
            throws UnableToCompleteException {
        return new ClientSerializerTypeFilter(blackWhiteList);
    }

    private void generateIsSerializableMethod(SourceWriter w) {
        w.println();
        // Write the method signature
        w.print("public boolean isSerializable(Class<?> clazz) {");
        w.indent();
        w.println("return SERIALIZER.getSerializationSignature(clazz) != null;");
        w.outdent();
        w.println("}");
    }

    /**
     * Generate any fields required by the proxy.
     *
     * @param srcWriter
     *         Source writer.
     */
    protected void generateFields(SourceWriter srcWriter) {
        String typeSerializerName = SerializationUtils.getTypeSerializerQualifiedName(serviceIntf);
        srcWriter.println("private static final %s SERIALIZER = new %s();",
                typeSerializerName, typeSerializerName);
        srcWriter.println();
    }

    /**
     * Generates the client's asynchronous proxy method.
     *
     * @param w
     *         Source writer.
     */
    protected void generateSerializeMethod(SourceWriter w) {

        w.println();

        // Write the method signature
        w.print("public String serialize(Object object) {");
        w.indent();
        NameFactory nameFactory = new NameFactory();
        w.println("try {");
        w.indent();

        String streamWriterName = nameFactory.createName("streamWriter");
        w.println("%s %s = new StringSerializationStreamWriter(SERIALIZER);",
                StringSerializationStreamWriter.class.getSimpleName(), streamWriterName);

        w.println(streamWriterName + ".writeObject(object);");
        w.println("return " + streamWriterName + ".toString();");
        w.outdent();
        w.print("} catch (SerializationException ");
        String exceptionName = nameFactory.createName("ex");
        w.println("%s) {", exceptionName);
        w.indent();
        w.println("throw new RuntimeException(%s.getMessage(), %s);", exceptionName, exceptionName);
        w.outdent();
        w.println("}");

        w.outdent();
        w.println("}");
    }

    /**
     * Generates the client's asynchronous proxy method.
     *
     * @param w
     *         Source writer.
     */
    protected void generateDeserializeMethod(SourceWriter w) {

        w.println();
        // Write the method signature
        w.print("public <T> T deserialize(String object) {");
        w.indent();
        NameFactory nameFactory = new NameFactory();
        w.println("try {");
        w.indent();

        String streamReaderName = nameFactory.createName("streamReader");
        w.println("%s %s = new StringSerializationStreamReader(SERIALIZER);",
                StringSerializationStreamReader.class.getSimpleName(), streamReaderName);
        w.println(streamReaderName + ".prepare(object);");
        w.println("return (T) " + streamReaderName + ".readObject();");
        w.outdent();
        w.print("} catch (SerializationException ");
        String exceptionName = nameFactory.createName("ex");
        w.println(exceptionName + ") {");
        w.indent();
        w.println("throw new RuntimeException(" + exceptionName + ".getMessage(), " + exceptionName + ");");
        w.outdent();
        w.println("}");

        w.outdent();
        w.println("}");
    }

    protected void generateTypeHandlers(TreeLogger logger, GeneratorContextExt context,
                                        SerializableTypeOracle typesSentFromBrowser)
            throws UnableToCompleteException {
        Event event = SpeedTracerLogger.start(CompilerEventType.GENERATOR_RPC_TYPE_SERIALIZER);

        TypeSerializerCreator tsc = new TypeSerializerCreator(
                logger, typesSentFromBrowser,
                typesSentFromBrowser, context,
                SerializationUtils.getTypeSerializerQualifiedName(serviceIntf),
                SerializationUtils.getTypeSerializerSimpleName(serviceIntf)
        );
        tsc.realize(logger);

        event.end();
    }

    protected String getProxySimpleName() {
        String[] name = Shared.synthesizeTopLevelClassName(serviceIntf, SUFFIX);
        return name[1];
    }

    protected Class<? extends SerializationStreamWriter> getStreamWriterClass() {
        return StringSerializationStreamWriter.class;
    }

    protected Class<? extends SerializationStreamReader> getStreamReaderClass() {
        return StringSerializationStreamReader.class;
    }

    private String getProxyQualifiedName() {
        String[] name = Shared.synthesizeTopLevelClassName(serviceIntf, SUFFIX);
        return name[0].length() == 0 ? name[1] : name[0] + "." + name[1];
    }

    private SourceWriter getSourceWriter(TreeLogger logger, GeneratorContextExt ctx,
                                         JClassType serviceAsync) {
        JPackage serviceIntfPkg = serviceAsync.getPackage();
        String packageName = serviceIntfPkg == null ? "" : serviceIntfPkg.getName();
        PrintWriter printWriter = ctx.tryCreate(logger, packageName, getProxySimpleName());
        if (printWriter == null) {
            return null;
        }

        ClassSourceFileComposerFactory composerFactory =
                new ClassSourceFileComposerFactory(packageName, getProxySimpleName());

        String[] imports = new String[] {
                getStreamWriterClass().getCanonicalName(),
                getStreamReaderClass().getCanonicalName(),
                SerializationStreamWriter.class.getCanonicalName(),
                SerializationStreamReader.class.getCanonicalName(),
                GWT.class.getCanonicalName(),
                SerializationException.class.getCanonicalName(),
        };

        for (String item : imports) {
            composerFactory.addImport(item);
        }

        composerFactory.addImplementedInterface(ISerializer.class.getCanonicalName());

        return composerFactory.createSourceWriter(ctx, printWriter);
    }

    @Override
    public String generate(TreeLogger logger, GeneratorContext context, String typeName)
            throws UnableToCompleteException {
        return create(logger, (GeneratorContextExt) context);
    }

    protected void addRoots(TreeLogger logger, TypeOracle typeOracle,
                            SerializableTypeOracleBuilder typesSentFromBrowserBuilder,
                            BlackWhiteList blackWhiteList)
            throws UnableToCompleteException {
        try {
            addRequiredRoots(logger, typeOracle, typesSentFromBrowserBuilder);

            addRootTypes(logger, typesSentFromBrowserBuilder, blackWhiteList);
        } catch (NotFoundException e) {
            logger.log(TreeLogger.ERROR, "Unable to find type referenced from serializer", e);
            throw new UnableToCompleteException();
        }
    }

    private void addRootTypes(TreeLogger logger, SerializableTypeOracleBuilder typesSentFromBrowser,
                              BlackWhiteList blackWhiteList)
            throws NotFoundException, UnableToCompleteException {

        for (JType type : blackWhiteList.getWhiteList()) {
            TreeLogger paramLogger = logger.branch(TreeLogger.DEBUG, "Serialization List", null);
            typesSentFromBrowser.addRootType(paramLogger, type);
        }
    }

    private void addRequiredRoots(TreeLogger logger, TypeOracle typeOracle,
                                  SerializableTypeOracleBuilder stob) throws NotFoundException {
        logger = logger.branch(TreeLogger.DEBUG, "Analyzing implicit types");

        Class<?>[] requiredTypes = new Class<?>[] {String.class, Number.class,
                Date.class, ClientSerializable.class, PrimitivesWhiteList.class};

        for (Class<?> cl : requiredTypes) {
            JClassType type = typeOracle.getType(cl.getCanonicalName());
            stob.addRootType(logger, type);
        }
    }

}
